import uiautomator2
import time
import json
import random
import os
import re
import requests
import threading
import base64
import subprocess
from typing import NamedTuple, IO, Optional

# uiautomator2.requests.urllib3.disable_warnings()


def print2(*args, **kwargs):
    pass


# uiautomator2.print = print2

# hierarchy 404问题
from xml.dom.minidom import parseString
import uiautomator2
from retry import retry
from uiautomator2.exceptions import NullPointerExceptionError


@retry(NullPointerExceptionError, delay=.5, tries=5, jitter=0.2)
def dump_hierarchy(self, compressed=False, pretty=False):
    content = self.jsonrpc.dumpWindowHierarchy(compressed, None)
    if pretty and "\n " not in content:
        xml_text = parseString(content.encode("utf-8"))
        content = xml_text.toprettyxml(indent='  ')
    return content


uiautomator2.Session.dump_hierarchy = dump_hierarchy


class ShellResponse(NamedTuple):
    output: str
    exit_code: int


class Device:

    def __init__(self, ip, port, udid='', multiapp='svx'):
        self.udid = udid
        self.ip = ip
        self.port = port
        self.multiapp = multiapp
        self.online = True

        self.connect = uiautomator2.UIAutomatorServer(ip, port)
        self.session = self.connect.session()
        self.last_online_time = time.time()
        self.offline_callback = None

    def __call__(self, *args, **kwagrs):
        return self.connect.__call__(*args, **kwagrs)

    @property
    def serial(self):
        if hasattr(self, '_serial') and self._serial:
            return self._serial
        else:
            serial = self.shell('getprop ro.serialno').output
            if serial:
                self._serial = serial.strip()
                return self._serial
            else:
                return ''

    @property
    def model(self):
        if hasattr(self, '_model') and self._model:
            return self._model
        else:
            model = self.shell('getprop ro.product.model').output
            if model:
                self._model = model.strip()
                return self._model
            else:
                return ''

    @property
    def sdk(self):
        if hasattr(self, '_sdk') and self._sdk:
            return self._sdk
        else:
            sdk = self.shell('getprop ro.build.version.sdk').output
            if sdk and sdk.strip():
                self._sdk = int(sdk.strip())
                return self._sdk
            else:
                return -1

    # 用于识别手机是否为芯片，芯片固定SIM8950LB01V01_A8
    @property
    def incremental(self):
        if hasattr(self, '_incremental') and self._incremental:
            return self._incremental
        else:
            incremental = self.shell('getprop ro.build.version.incremental').output
            if incremental and incremental.strip():
                self._incremental = incremental.strip()
                return self._incremental
            else:
                return ''

    # sim卡运营商号码
    @property
    def sim_operator(self):
        if hasattr(self, '_sim_operator') and self._sim_operator:
            return self._sim_operator
        else:
            sim_operator = self.shell('getprop gsm.sim.operator.numeric').output
            if sim_operator and sim_operator.strip():
                self._sim_operator = sim_operator.strip()
                return self._sim_operator
            else:
                return ''

    @property
    def sdcard0(self):
        if hasattr(self, '_sd_path') and self._sd_path:
            return self._sd_path
        else:
            if self.model == 'PP5200' or self.model == 'Nexus 5':
                self._sd_path = '/storage/sdcard0/'
            else:
                self._sd_path = '/storage/emulated/0/'
            return self._sd_path

    @property
    def display(self):
        if hasattr(self, '_display') and self._display:
            return self._display
        else:
            display_info = self.shell('wm size').output
            if display_info:
                m = re.search(r'(\d+)x(\d+)', display_info)
                if m:
                    self._display = (int(m.group(1)), int(m.group(2)))
                    return self._display

    def refresh_port(self, port):
        self.port = port
        self.connect = uiautomator2.UIAutomatorServer(self.ip, port)
        self.session = self.connect.session()

    def offline(self):
        self.online = False
        if self.offline_callback:
            self.offline_callback()

    def add_offline_callback(self, callback):
        self.offline_callback = callback

    def shell(self, cmd, timeout: int = 60):
        try:
            output, code = self.connect.shell(cmd, timeout=timeout)
            return ShellResponse(output, code)
        except Exception as e:
            '''
            if "由于目标计算机积极拒绝" in str(e) and "7912" in str(e):
                device_ip = e.args[0].pool.host
                adb_process = subprocess.Popen(
                    ["adb", "-s", device_ip, "shell"],
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True
                )
                command = "killall agc-client;/data/local/tmp/agc-client server --port=7912 -d -a 0.0.0.0;\n"
                stdout, stderr = adb_process.communicate(input=command)
                '''
            print(e)
        return ShellResponse('', 1)


    def get_page(self) -> str:
        try:
            return self.session.dump_hierarchy(compressed=False)
        except Exception as e:
            print('Get page exception:', e)
        return ''

    def dump_activity(self):
        try:
            return self.shell('dumpsys activity top').output
        except Exception as e:
            print('Get page exception:', e)
        return ''

    def unlock(self):
        self.screen_on()
        self.swipe_up(0.8)
        self.shell("settings put system screen_off_timeout 2147483647")
        self.shell('input keyevent KEYCODE_BACK')
        self.shell('input keyevent KEYCODE_HOME')
        self.click('CANCEL')

    def screen_on(self):
        result = self.shell('dumpsys power|grep state').output
        if not 'ON' in result:
            self.shell('input keyevent KEYCODE_POWER')

    @property
    def is_root(self):
        if hasattr(self, '_is_root') and self._is_root:
            return self._is_root
        else:
            result = self.shell('whoami').output
            if 'root' in result:
                self._is_root = True
            else:
                self._is_root = False
            return self._is_root

    @property
    def imei(self):
        if hasattr(self, '_imei') and self._imei:
            return self._imei
        elif self.cat(self.sdcard0 + 'imei.txt'):
            imei = self.cat(self.sdcard0 + 'imei.txt')
            if imei and imei.strip():
                self._imei = imei
                return imei
            else:
                os.remove(self.sdcard0 + 'imei.txt')
        else:
            result = self.shell('service call iphonesubinfo 1').output
            digit_parts = re.findall(r'\'([\S\s]+?)\'', result)
            imei = ''
            if digit_parts:
                for each in digit_parts:
                    imei += each.replace('.', '')
                imei = imei.strip()

            # 获取不到(如pptv没权限)，尝试用msp生成的deviceinfo.txt
            # Note(R3296):
            # - deviceId 取值是一下的任意一个
            #   - ESN [8]
            #   - MEID [14]
            #   - IMEI [15,17]
            if len(imei) < 8 or len(imei) > 17:
                imei = str(int.from_bytes(bytes.fromhex(self.udid[:18].replace("-", "")), "little"))[:15].zfill(15)

            self.shell(f'echo -n {imei}>{self.sdcard0}/imei.txt')
            self._imei = imei
            return imei

    def clear_clipboard(self) -> None:
        """
        清空
        :return: None
        """
        self.session.set_clipboard(None, "")

    def clipboard(self):
        return self.session.clipboard


    def get_real_ip(self, expired_seconds=0):
        # http://47.88.217.154:8011/get_ip.php
        result = self.cat(f'{self.sdcard0}real_ip.txt')
        if result and '|' in result:
            time_stamp, ip = result.split('|')
            now = time.time()
            if now - int(time_stamp) < expired_seconds:
                return ip

        self.shell(f'rm {self.sdcard0}ip_temp.txt')
        self.shell(
            f'wget -T 10 -O {self.sdcard0}ip_temp.txt http://221.120.163.66:8080/mpsf_service/myip --header="User-Agent: Mozilla/5.0"')
        ip = self.cat(f'{self.sdcard0}ip_temp.txt')
        if ip:
            now = int(time.time())
            self.shell(f'echo -n "%d|%s" > {self.sdcard0}real_ip.txt' % (now, ip))
            return ip
        return ''

    def get_sim_name(self, slot=1):
        sim_name = ''
        sim_info = self.shell('dumpsys telephony.registry|grep mServiceState').output.lower().strip().split('\n')
        pattern = r'(metfone|smart|cellcard|mobilis|djezzy|ooredoo)'
        # 获取卡槽一
        if slot == 1:
            m = re.search(pattern, sim_info[0])
            if m:
                sim_name = m.group()
            elif len(sim_info) >= 2:
                m = re.search(pattern, sim_info[1])
                if m:
                    sim_name = m.group()
        # 获取卡槽二
        elif slot == 2:
            if len(sim_info) >= 2:
                m = re.search(pattern, sim_info[1])
                if m:
                    sim_name = m.group()
                else:
                    m = re.search(pattern, sim_info[0])
                    if m:
                        sim_name = m.group()
        return sim_name

    def call(self, number):
        self.shell('am start -a android.intent.action.CALL -d tel:%s' % (number))

    __attrs = ('resourceId', 'text', 'description', 'className', 'textContains')
    __attrs_matches = ('textMatches', 'descriptionMatches')
    __attrs_contains = ('textContains', 'descriptionContains')

    def click(self, tag, index=0, ignore_case=True):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    uiobjs[index].click()
                    return True
            except:
                break
        if ignore_case:
            for attr in Device.__attrs_matches:
                if '?' in tag:
                    tag = tag.replace('?', '\?')
                uiobjs = self.connect(**{attr: '(?i)' + tag})
                try:
                    if uiobjs.exists():
                        uiobjs[index].click()
                        return True
                except:
                    break
        return False

    def click_contains(self, tag, index=0):
        for attr in Device.__attrs_contains:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    uiobjs[index].click()
                    return True
            except:
                break
        return False

    def click_child(self, tag, index, child_index):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    uiobjs[index].child()[child_index].click()
                    return True
            except:
                break
        return False

    def get_object_num(self, tag):
        page = self.get_page()
        nodes = re.findall(r'<node[\s\S]+?/?>', page)
        if nodes:
            targets = [node for node in nodes if tag in node]
            return len(targets)
        return 0

    def get_child_count(self, tag, index):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    return uiobjs[index].child().count
            except:
                break
        return 0

    def click_xy(self, x, y):
        if x < 1:
            x *= self.display[0]
        if y < 1:
            y *= self.display[1]
        self.shell(f'input tap {x} {y}')

    def long_click(self, tag, index=0):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    uiobjs[index].long_click()
                    return True
            except:
                break

    def long_click_xy(self, x, y, duration=0.5):
        self.connect.long_click(x, y, duration)

    def toast(self, text, duration: float = 1.0):
        self.session.make_toast(text, duration)

    __attrs_all = ('textContains', 'descriptionContains', 'resourceId', 'className')

    def has(self, *tags):
        def has1(tag):
            for attr in Device.__attrs_all:
                uiobjs = self.connect(**{attr: tag})
                try:
                    if uiobjs.exists():
                        return True
                except:
                    break
        return all([has1(tag) for tag in tags])

    def back(self):
        self.shell('input keyevent BACK')

    def home(self):
        self.shell('input keyevent HOME')

    def enter(self):
        self.shell('input keyevent ENTER')

    def set_text(self, tag, text, index=0):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    uiobjs[index].set_text(text)
                    return True
            except:
                break
        return False

    def input_text(self, text):
        self.shell('input text "' + text + '"')

    # base on agc.apk(com.github.uiautomator)
    def input_text2(self, text):
        text_b64encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
        self.shell(f'am broadcast -a ADB_INPUT_B64 --es msg {text_b64encoded}')
        time.sleep(0.5)

    def set_text_by_input_text(self, tag, text, index=0):
        self.click(tag, index)
        time.sleep(0.5)
        self.clear_text()
        self.input_text2(text)

    def get_text(self, tag, index=0):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    return uiobjs[index].get_text()
            except:
                break
        return ''


    def clear_text(self, clear_len=0):
        if clear_len > 0:
            cmd = 'input keyevent 123 '
            cmd += ' 67' * clear_len
            self.shell(cmd)
        else:
            self.shell(
                'input keyevent --longpress 123 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67')

    def swipe(self, x1, y1, x2, y2, duration=1):
        if x1 < 1:
            x1 *= self.display[0]
        if y1 < 1:
            y1 *= self.display[1]
        if x2 < 1:
            x2 *= self.display[0]
        if y2 < 1:
            y2 *= self.display[1]



        self.shell(f'input swipe {x1} {y1} {x2} {y2} {int(duration * 1000)}')

    def swipe_up(self, percent=0.5, duration=1):
        display = self.display
        self.shell(f'input swipe {display[0] / 2} {display[1] * percent} {display[0] / 2} 0 {int(1000 * duration)}')

    def swipe_points(self, points, duration=0.5):
        return self.connect.jsonrpc.swipePoints(points, duration)



    def swipe_down(self, percent=0.5, duration=1):
        display = self.display
        self.shell(
            f'input swipe {display[0] / 2} {display[1] * (1 - percent)} {display[0] / 2} {display[1]} {int(1000 * duration)}')

    def get_point(self, tag, index=0):
        for attr in Device.__attrs:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    return uiobjs[index].center()
            except:
                break
        return 0, 0

    def get_point_percent(self, tag, index=0):
        x, y = self.get_point(tag, index)
        w, h = self.display
        return x / w, y / h

    def get_point_contains(self, tag, index=0, percent=False):
        for attr in Device.__attrs_contains:
            uiobjs = self.connect(**{attr: tag})
            try:
                if uiobjs.exists():
                    if percent:
                        wx, wy = self.display
                        x, y = uiobjs[index].center()
                        return x / wx, y / wy
                    else:
                        return uiobjs[index].center()
            except:
                break
        return 0, 0

    def cat(self, path):
        data = self.shell("cat " + path).output
        if data and not "No such file or directory" in data:
            return data
        return None

    def push(self, src, dst):
        try:
            self.connect.push(src, dst)
        except:
            return False
        return True

    def push_bytes(self, data: bytes, dst):
        res =requests.post(f'http://{self.ip}:{self.port}/upload' + dst, data={"mode": "0644"}, files={"file": data}, timeout=70)
        print(res.status_code, res.text)

    def push_limit(self, src: str, dst: str, byte_pre_sec: int = 1024 * 1024, mode=0o644):
        self.connect.push_limit(src, dst, byte_pre_sec, mode)

    def pull_bytes(self, src: str):
        return self.connect.pull_content(src)

    def pull(self, src: str, dst: str):
        return self.connect.pull(src, dst)

    def upload_image(self, src, dst, user=0):

        if self.multiapp == 'sys':
            if dst.startswith('/sdcard'):
                dst = dst.replace('/sdcard', f'/storage/emulated/{user}')
            elif dst.startswith('/storage/emulated/0'):
                dst = dst.replace('/storage/emulated/0', f'/storage/emulated/{user}')
        else:
            user = 0

        self.shell('rm ' + dst)
        self.shell(f'am broadcast --user {user} -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://{dst}')
        time.sleep(0.5)
        pushOK = self.push(src, dst)
        self.shell(f'am broadcast --user {user} -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://{dst}')
        time.sleep(1)
        return pushOK

    # 同步
    upload_lock = threading.Lock()

    def upload_and_delete_image(self, pc_path, sd_path):
        with Device.upload_lock:
            if os.path.exists(pc_path):
                included_extensions = ['jpg', 'jpeg', 'bmp', 'png', 'gif', 'mp4']
                files = [fn for fn in os.listdir(pc_path) if any(fn.endswith(ext) for ext in included_extensions)]
                if files:
                    random_file = os.path.join(pc_path, random.choice(files))
                    update = self.upload_image(random_file, sd_path)
                    try:
                        os.remove(random_file)
                    except:
                        pass
                    return update
        return False

    def clear_media(self, user=0):
        self.shell(f'content delete --uri content://media/external/images/media --user {user} --where "_id!=-1"')
        self.shell(f'content delete --uri content://media/external/video/media --user {user} --where "_id!=-1"')

    def clear_contact(self):
        self.run_msp(True)
        self.shell('am broadcast -a com.msp.CLEAR_CONTACTS')
        time.sleep(1)

    # 不支持7.0及以上
    def clear_sms(self):
        self.run_msp(True)
        self.shell('am broadcast -a com.msp.CLEAR_SMS')
        time.sleep(1)

    def screenshot(self, pc_path):
        filename = os.path.basename(os.path.normpath(pc_path))
        address = 'http://' + self.ip + ':' + str(self.port) + '/screenshot/0'
        try:
            print()
            r = requests.get(address, timeout=10)
            if r.status_code == 200:
                dirname = os.path.dirname(pc_path)
                os.makedirs(dirname, exist_ok=True)
                with open(pc_path, 'wb') as f:
                    f.write(r.content)
                    return True
            else:
                raise Exception('Status is not 200')
        except Exception as e:
            print('Screenshot failed:', pc_path, e)
        return False

    def switch_airplane_mode(self, is_root=False):
        # 手机系统大于等于7的需要到settings里开关
        if is_root or self.sdk < 24:
            self.shell('settings put global airplane_mode_on 1')
            self.shell('am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true')
            time.sleep(1)
            self.shell('settings put global airplane_mode_on 0')
            self.shell('am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false')
            time.sleep(5)
        else:
            self._switch_airplane_mode_in_settings()

    def _switch_airplane_mode_in_settings(self):
        self.shell('am start -a android.settings.AIRPLANE_MODE_SETTINGS')
        index = 0
        # 兼容芯片
        if self.incremental == 'SIM8950LB01V01_A8':
            index = 1

        has_clicked = False
        for _ in range(20):
            status = self.get_text('android.widget.Switch', index)
            if status:
                if 'ON' in status:
                    self.click('android.widget.Switch', index)
                    time.sleep(1)
                    has_clicked = True
                else:
                    if has_clicked:
                        return True
                    else:
                        self.click('android.widget.Switch', index)
            else:
                self.click('OK')
                self.swipe_down(0.5)
            time.sleep(1)
        return False

    def enable_airplane_mode(self, enable=True):

        index = 0
        # 兼容机箱8.0手机芯片
        if self.incremental == 'SIM8950LB01V01_A8':
            index = 1

        self.shell('am start -a android.settings.AIRPLANE_MODE_SETTINGS')
        for _ in range(20):
            status = self.get_text('android.widget.Switch', index)
            if status:
                if enable:
                    if 'OFF' in status.upper():
                        self.click('android.widget.Switch', index)
                    else:
                        return True
                else:
                    if 'ON' in status.upper():
                        self.click('android.widget.Switch', index)
                    else:
                        return True
            else:
                self.swipe_down(0.5)
            time.sleep(1)
        return False

    def get_app_version_code(self, package):
        result = self.shell('dumpsys package ' + package + '|grep versionCode').output
        version_code = -1
        m = re.search(r'versionCode=(\d+)', result)
        if m:
            version_code = int(m.group(1))
        return version_code

    def get_packages(self, third=False):
        result = self.shell(' '.join(['pm list package', '-3' if third else ''])).output
        return re.findall(r'package:([\S]+)?\s', result)

    def check_network_curl(self, retry: int = 30):
        addr_204 = ["http://www.gstatic.com/generate_204",
                    "http://cp.cloudflare.com/generate_204",
                    "http://www.qualcomm.cn/generate_204",
                    "http://g.cn/generate_204"]
        for i in range(retry):
            addr = addr_204[i % len(addr_204)]
            cmd = 'curl -o /dev/null -s -w "%{http_code}" ' + addr + ' --connect-timeout 3 2> /dev/null'
            result, code = self.shell(cmd, timeout=10)
            if code == 0 and result.startswith("204"):
                return True
            time.sleep(1)
        return False

    def get_battery_temperature(self):
        result = self.shell('dumpsys battery|grep temperature').output
        if result:
            match = re.findall(r'\d+', result)
            if match:
                return int(match[0]) / 10
        return None

    def run_app(self, package, activity, stop=True, virtual=99999):
        permissions = ['android.permission.READ_PHONE_STATE',
                       'android.permission.READ_EXTERNAL_STORAGE',
                       'android.permission.WRITE_EXTERNAL_STORAGE',
                       'android.permission.ACCESS_FINE_LOCATION',
                       'android.permission.ACCESS_COARSE_LOCATION',
                       'android.permission.BODY_SENSORS']
        if self.multiapp == 'svx':
            if virtual > -1 and virtual < 100:
                if stop:
                    self.shell('am force-stop io.virtualapp.sandvxposed')
                self.grant_permissions('io.virtualapp.sandvxposed', permissions)
                self.shell('am start -n io.virtualapp.sandvxposed/io.virtualapp.splash.SplashActivity')
                time.sleep(8)
                self.shell(
                    f'am startservice -a io.virtualapp.j719.service --es cmd startapp --es package {package} --ei user {virtual}')
            else:
                if stop:
                    self.stop_app(package)
                if activity:
                    self.shell('am start -n ' + package + '/' + activity)
                else:
                    self.shell(
                        f'am start -n com.github.uiautomator/.PkgLauncherActivity --es package {package} --user 0')
        elif self.multiapp == 'xj':
            if virtual > -1 and virtual < 100:
                if stop:
                    if self.shell('which su'):
                        self.shell(
                            'su system am stop-service -n com.xzj.multiapps/com.lody.virtual.client.stub.KeepAliveService; am force-stop com.xzj.multiapps')
                    else:
                        # 多执行一次，确保杀死
                        self.shell('am force-stop com.xzj.multiapps')
                        self.shell('am force-stop com.xzj.multiapps')
                self.grant_permissions('com.xzj.multiapps', permissions)
                self.shell('am start -n com.xzj.multiapps/com.xzj.multiapps.home.UseYinsiActivity')
                for _ in range(30):
                    time.sleep(1)
                    if 'com.xzj.multiapps:id/bo' in self.get_page():
                        break
                # 临时处理
                if package == 'com.facebook.katana':
                    self.shell(
                        f'am startservice -a io.virtualapp.service --es cmd start --es activity com.facebook.katana/com.facebook.katana.IntentUriHandler --ei user {virtual}')
                else:
                    self.shell(
                        f'am startservice -a io.virtualapp.service --es cmd startapp --es package {package} --ei user {virtual}')
            else:
                if stop:
                    self.stop_app(package)
                if activity:
                    self.shell('am start -n ' + package + '/' + activity)
                else:
                    self.shell(
                        f'am start -n com.github.uiautomator/.PkgLauncherActivity --es package {package} --user 0')
        elif self.multiapp == 'sys':
            if stop:
                self.stop_app(package)
            if virtual == 99999:
                virtual = 0
            self.shell(f'am start-user {virtual} && am start --user {virtual} -n {package}/{activity}')

    def stop_app(self, package):
        if self.multiapp == 'sys':
            self.shell('am force-stop --user all ' + package)
        else:
            # 若是desay手机，先关闭应用商店，防止后期弹窗
            if self.model == 'DESAY D18':
                self.shell('am force-stop com.zhuoyi.market')
                self.shell('am force-stop com.zhuoyi.security.service')
                self.shell('am force-stop com.zhuoyi.security.lite')
            self.shell('am force-stop ' + package)

    # 若提供app_name，则需要清除settings里对应app_name的账号，仅适用于svx和xj外层
    def clear_app(self, package, virtual=99999, app_name=None):
        # 系统多开
        if self.multiapp == 'sys' or (self.incremental == 'SIM8950LB01V01_A8' and virtual == 99999):
            return self.clear_app_sys(package, virtual)
        # sandvxposed多开
        elif self.multiapp == 'svx':
            return self.clear_app_svx(package, virtual, app_name)
        # 星极多开
        elif self.multiapp == 'xj':
            return self.clear_app_xj(package, virtual, app_name)
        return False

    # 清除app，仅适用sandvxposed多开
    def clear_app_svx(self, package, virtual, app_name):
        if virtual > -1 and virtual < 100:
            self.shell('am start -n io.virtualapp.sandvxposed/io.virtualapp.splash.SplashActivity')
            time.sleep(8)
            self.shell(
                f'am startservice -a io.virtualapp.j719.service --es cmd clear --es package {package} --ei user {virtual}')
            return True
        else:
            if app_name:
                if not self.remove_account_in_settings(app_name):
                    return False
            result = self.shell('pm clear ' + package).output
            if 'success' in result.lower():
                return True
        return False

    # 清除app，仅适用星极多开
    def clear_app_xj(self, package, virtual, app_name):
        if virtual > -1 and virtual < 100:
            self.shell('am start -n com.xzj.multiapps/com.xzj.multiapps.home.UseYinsiActivity')
            time.sleep(8)
            self.shell(
                f'am startservice -a io.virtualapp.service --es cmd clear --es package {package} --ei user {virtual}')
            return True
        else:
            if app_name:
                if not self.remove_account_in_settings(app_name):
                    return False
            result = self.shell('pm clear ' + package)
            if 'success' in result.lower():
                return True
        return False

    # 清除app，仅适用系统多开
    def clear_app_sys(self, package, virtual):
        if virtual == 99999:
            virtual = 0
        uid = self.shell(f'pm get-uid --user {virtual} {package}').strip().output
        if uid:
            ret = self.shell(f'su {uid} pm remove-account --user {virtual} {package}').output
            print(f'su {uid} pm remove-account --user {virtual} {package}', ret)
            result = self.shell(f'pm clear --user {virtual} {package}').output
            if 'success' in result.lower():
                return True
        return False

    # 清空账号
    # 适用多开环境:svx和xj的外层。
    # 适用app:facebook,twitter,messenger。
    def remove_account_in_settings(self, app_name):
        for _ in range(20):
            page = self.get_page()
            if 'Add account' in page and ('Accounts' in page or 'ACCOUNTS' in page):
                if f'"{app_name}"' in page:
                    self.click(app_name)
                    time.sleep(3)
                    self.click('More options')
                    time.sleep(3)
                    self.click('Remove account')
                    time.sleep(3)
                    self.click('Remove account')
                    time.sleep(1)
                else:
                    self.stop_app('com.android.settings')
                    print(self.imei, f'{app_name} remove account succeed!')
                    return True
            elif 'Remove account' in page:
                self.click('Remove account')
            elif 'isn\'t responding' in page:
                self.click('OK')
            elif 'Auto-sync data' in page:
                self.shell('input keyevent KEYCODE_BACK')
            else:
                self.stop_app('com.android.settings')
                time.sleep(1)
                self.shell('am start -a android.settings.SYNC_SETTINGS')
                time.sleep(2)
        return False

    # 检测app是否安装到位，包括多开及其内部对应层数app
    APP_OK = 0
    APP_UNINSTALL = 1
    APP_NO_VIRTUAL = 2
    APP_USER_NOT_MATCH = 3

    def check_app(self, package, virtual):
        """
        0:成功, 1:没安装apk, 2:没安装多开, 3:多开里没有对应的层数
        """
        # 系统多开
        if self.multiapp == 'sys' or (self.incremental == 'SIM8950LB01V01_A8' and virtual == 99999):
            return self.check_app_sys(package, virtual)
        # sandvxposed多开
        elif self.multiapp == 'svx':
            return self.check_app_svx(package, virtual)
        # 星极多开
        elif self.multiapp == 'xj':
            return self.check_app_xj(package, virtual)
        return self.APP_UNINSTALL

    def check_app_sys(self, package, virtual):
        if virtual == 99999:
            virtual = 0
        result = self.shell(
            f'for i in $(seq 0 30); do [ -d /data/user/$i/{package} ] && echo -n $i,; done; echo -n ""').output
        if str(virtual) in result.split(','):
            return self.APP_OK
        return self.APP_UNINSTALL

    def check_app_svx(self, package, virtual):
        if virtual == 99999:
            if self.get_app_version_code(package) == -1:
                return self.APP_UNINSTALL
        else:
            va_version = self.get_app_version_code('io.virtualapp.sandvxposed')
            if va_version == -1:
                return self.APP_NO_VIRTUAL
            else:
                user_num = self.get_multiapp_user_num(package)
                if virtual >= user_num:
                    return self.APP_USER_NOT_MATCH
        return self.APP_OK

    def check_app_xj(self, package, virtual):
        if virtual == 99999:
            if self.get_app_version_code(package) == -1:
                return self.APP_UNINSTALL
        else:
            va_version = self.get_app_version_code('com.xzj.multiapps')
            if va_version == -1:
                return self.APP_NO_VIRTUAL
            else:
                user_num = self.get_multiapp_user_num(package)
                if virtual >= user_num:
                    return self.APP_USER_NOT_MATCH
        return self.APP_OK

    def clean_background(self):
        self.shell('input keyevent KEYCODE_APP_SWITCH')
        time.sleep(2)

        self.click('com.android.systemui:id/clear_button')
        self.click('com.android.systemui:id/clear_recent_button')
        self.click('com.android.systemui:id/kewang_clear_recent_button')
        self.click('CLEAR ALL')
        self.click('CLOSE ALL')
        # 芯片:app奔溃系统提示
        self.click('Close app')
        time.sleep(2)

    def read_sms(self, limit: int = 1):
        return self.connect.jsonrpc_call(f'http://{self.ip}:{self.port}/jsonrpc/0', 'readSMS', params=[limit])


    # 文字识别
    def get_point_ocr(self, text, activity=None, center=True):
        x1, y1, x2, y2 = 0, 0, 0, 0
        coordinates = self.image_detect(method_id=1, text=text, activity=activity)
        if coordinates:
            x1, y1, x2, y2 = tuple(map(int, next(iter(coordinates.values())).split(',')))
        if center:
            return (x1 + x2) / 2, (y1 + y2) / 2
        else:
            return x1, y1, x2, y2

    def get_multiapp_user_num(self, package):
        if self.multiapp == 'sys':
            result = self.shell(
                f'for i in $(seq 0 30); do [ -d /data/user/$i/{package} ] && echo -n $i,; done; echo -n ""').output
            if result:
                return len(result.split(','))
        else:
            multiapp_pkg = None
            activity = None
            service = None
            if self.multiapp == 'svx':
                multiapp_pkg = 'io.virtualapp.sandvxposed'
                activity = 'io.virtualapp.sandvxposed/io.virtualapp.splash.SplashActivity'
                service = 'io.virtualapp.j719.service'
            elif self.multiapp == 'xj':
                multiapp_pkg = 'com.xzj.multiapps'
                activity = 'com.xzj.multiapps/com.xzj.multiapps.home.UseYinsiActivity'
                service = 'io.virtualapp.service'

            # 授权免弹窗
            self.shell(f'am force-stop {multiapp_pkg}')
            permissions = ['android.permission.READ_PHONE_STATE',
                           'android.permission.READ_EXTERNAL_STORAGE',
                           'android.permission.WRITE_EXTERNAL_STORAGE',
                           'android.permission.ACCESS_FINE_LOCATION',
                           'android.permission.ACCESS_COARSE_LOCATION',
                           'android.permission.BODY_SENSORS']
            self.grant_permissions(multiapp_pkg, permissions)

            self.shell(f'am start -n {activity}')
            time.sleep(8)
            self.shell(f'am startservice -a {service} --es cmd users --es package {package}')

            users = None
            for _ in range(6):
                time.sleep(0.5)
                users = self.cat(self.sdcard0 + 'virtual-app/users.txt')
                if users:
                    break

            if not users or 'None' in users:
                return 0
            else:
                try:
                    user = json.loads(users)
                    return len(user['users'].strip().split(' '))
                except:
                    return 0
        return 0

    def grant_permissions(self, package, permissions):
        granted_permissions = self.shell(
            f"dumpsys package {package} | egrep -o '[^ ]+: granted=true'  | awk -F ':' '{{print $1}}'").output
        for permission in set(permissions).difference(granted_permissions.split("\n")):
            self.shell(f'pm grant {package} {permission}')

    def query(self, uri, columns, where, args, order):
        # am broadcast -a com.msp.QUERY --es uri "uri"  --esa columes "columes"  --es where "where"  --esa args "args"  --es order "order"
        query = ''
        self.shell(f'rm {self.sdcard0}msp/query.txt')
        cmd_uri = f' --es uri \"{uri}"' if uri else ''
        cmd_columns = f' --esa columes \"{columns}\"' if columns else ''
        cmd_where = f' --es where \"{where}\"' if where else ''
        cmd_args = f' --esa args \"{args}\"' if args else ''
        cmd_order = f' --es order \"{order}\"' if args else ''
        cmd = f'am broadcast -a com.msp.QUERY{cmd_uri}{cmd_columns}{cmd_where}{cmd_args}{cmd_order}'

        self.shell(cmd)

        for i in range(10):
            cat = self.cat(f'{self.sdcard0}msp/query.txt')
            if not cat or 'No such file or directory' in str(cat):
                time.sleep(0.5)
            else:
                query = cat
                break

        return query

    def get_note(self):
        note = ''
        url = 'http://' + self.ip + ':' + str(self.port) + '/info'
        try:
            r = requests.get(url, timeout=5)
            if r.status_code == 200:
                info = r.json()
                if 'note' in info:
                    note = info['note']
        except Exception as e:
            print(f'Exception: {e}')
        return note
